feat(analytics): count SPA pageviews incl. each opened anchor#562
Conversation
GoatCounter's count.js only fires on the initial full page load, so client-side navigation (opening anchors, switching to docs/contracts) was not counted — the stats only showed entry pages. Report a pageview from the router on every client-side route change, skipping the first handleRoute() so the initial load (already auto-counted by count.js) is not double-counted. Each opened anchor (/anchor/:id) is now recorded with its path and readable title, giving per-anchor view counts. Privacy is unchanged: path only (no query string), no personal data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WalkthroughDer Router wird um GoatCounter-Integration für Pageview-Tracking erweitert. Eine neue ChangesGoatCounter-Pageview-Tracking für SPA-Navigation
Sequence DiagramsequenceDiagram
participant User as Benutzer
participant Router
participant trackPageview
participant GoatCounter
User->>Router: navigiert zur Route
Router->>trackPageview: ruft trackPageview() auf
Note over trackPageview: überspringt ersten Aufruf<br/>(firstRouteHandled-Flag)
trackPageview->>GoatCounter: window.goatcounter.count({path, title})
GoatCounter-->>trackPageview: Tracking registriert
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
website/src/utils/router.js (1)
212-220:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFehlender
trackPageview()-Aufruf im Fallback-Pfad.Wenn eine Route nicht gefunden wird und zur Homepage zurückgefallen wird, fehlt der
trackPageview()-Aufruf. Dies führt dazu, dass Navigationen zu unbekannten Routes nicht in GoatCounter gezählt werden.🔧 Vorgeschlagener Fix
} else { // Default to home if route not found const homeHandler = routes.get('/') if (typeof homeHandler === 'function') { currentRoute = '/' document.title = ROUTE_TITLES['/'] homeHandler() + trackPageview() } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@website/src/utils/router.js` around lines 212 - 220, When falling back to the homepage in the router (the else branch that gets homeHandler via routes.get('/')), add a call to trackPageview() after setting currentRoute = '/' and document.title = ROUTE_TITLES['/'] and before or after invoking homeHandler(); ensure the trackPageview() invocation is present so unknown-route navigations are recorded by GoatCounter (refer to currentRoute, ROUTE_TITLES, homeHandler(), and trackPageview()).
🧹 Nitpick comments (1)
website/src/utils/router.test.js (1)
39-51: ⚡ Quick winTest-Coverage für
title-Feld und explizite Verifikation des Skip-Verhaltens fehlt.Der Test prüft nur das
path-Feld, obwohl laut Implementierung auchtitleangoatcounter.count()übergeben wird. Außerdem wird nicht explizit verifiziert, dass die erste Navigation tatsächlich übersprungen wird.🧪 Vorgeschlagene Verbesserung der Test-Abdeckung
- it('reports a GoatCounter pageview with the path on SPA navigation', () => { + it('reports a GoatCounter pageview with path and title on SPA navigation', () => { window.goatcounter = { count: vi.fn() } addRoute('/gc-test', vi.fn()) // The very first route of the run is skipped by design (count.js auto-counts // the initial load); clear and navigate again so we assert a later change. navigate('/gc-test') - window.goatcounter.count.mockClear() + expect(window.goatcounter.count).not.toHaveBeenCalled() navigate('/gc-test') expect(window.goatcounter.count).toHaveBeenCalledWith( - expect.objectContaining({ path: '/gc-test' }) + expect.objectContaining({ + path: '/gc-test', + title: expect.any(String) + }) ) delete window.goatcounter })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@website/src/utils/router.test.js` around lines 39 - 51, The test only asserts the path but must also assert the title and that the initial navigation was skipped: after setting window.goatcounter = { count: vi.fn() } and calling addRoute('/gc-test', ...), explicitly call navigate('/gc-test') and assert window.goatcounter.count was not called (verifying the skip behavior), then perform the second navigate and assert window.goatcounter.count was called with an objectContaining both { path: '/gc-test', title: 'GC Test' } (or the expected document/title value used by your router); reference the existing helpers addRoute and navigate and the mock window.goatcounter.count when adding these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@website/src/utils/router.js`:
- Around line 212-220: When falling back to the homepage in the router (the else
branch that gets homeHandler via routes.get('/')), add a call to trackPageview()
after setting currentRoute = '/' and document.title = ROUTE_TITLES['/'] and
before or after invoking homeHandler(); ensure the trackPageview() invocation is
present so unknown-route navigations are recorded by GoatCounter (refer to
currentRoute, ROUTE_TITLES, homeHandler(), and trackPageview()).
---
Nitpick comments:
In `@website/src/utils/router.test.js`:
- Around line 39-51: The test only asserts the path but must also assert the
title and that the initial navigation was skipped: after setting
window.goatcounter = { count: vi.fn() } and calling addRoute('/gc-test', ...),
explicitly call navigate('/gc-test') and assert window.goatcounter.count was not
called (verifying the skip behavior), then perform the second navigate and
assert window.goatcounter.count was called with an objectContaining both { path:
'/gc-test', title: 'GC Test' } (or the expected document/title value used by
your router); reference the existing helpers addRoute and navigate and the mock
window.goatcounter.count when adding these assertions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 57f6ec3f-59bd-4931-a98e-75c17bcaebba
📒 Files selected for processing (2)
website/src/utils/router.jswebsite/src/utils/router.test.js
document.referrer is fixed for the whole SPA session, so every in-app pageview we send (LLM-Coding#562/LLM-Coding#565) re-credited the entry referrer — e.g. a single visitor arriving from heise and browsing 20 anchors made heise's referrer list show all 20 paths. Report the referrer only on the initial load (handled by count.js); client-side route changes now send an empty referrer via a callback. External referrers are credited to the landing page only, matching the intuitive "where did this visit come from" meaning. Verified with GoatCounter's get_data: with document.referrer = heise, the landing keeps r="https://www.heise.de" while an SPA anchor view yields r="". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Why
Follow-up to #559. GoatCounter's
count.jsonly fires on the initial full page load, so the stats only showed entry pages — in-app navigation (opening anchors, switching to /contracts, /about, docs) wasn't counted. In particular we couldn't see which anchors users are interested in.What
Report a GoatCounter pageview from the router on every client-side route change (
handleRoute), with the resolvedpathandtitle:/anchor/:idis now counted with its readable title → per-anchor view counts.handleRoute()is skipped (the initial load is already auto-counted bycount.js), so no double-counting.window.goatcounter?.count), so nothing breaks if the script is blocked/not loaded.Privacy unchanged: path only (no query string), no personal data — same as the existing
path/referrerconfig.Verification
goatcounter.count({ path: … })(router.test.js).goatcounter.count:/anchor/conways-law→{ path: "/Semantic-Anchors/anchor/conways-law", title: "Conways Law — Semantic Anchors" }/contracts→{ path: "/Semantic-Anchors/contracts", title: "Semantic Contracts — Semantic Anchors" }🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
Neue Funktionen
Tests